/**
 * 观察者模式
 * 经典案例：vue数据双向绑定原理
 */

 // vue数据双向绑定原理
 // 实现observer: 对需要监听的数据对象进行遍历，给属性加上定制的getter和setter函数，一旦这个对象的某个属性发生变化，就会触发setter函数，进而通知到订阅者，setter函数就是监听器
 function observer(target) {
     if (target && typeof target === 'object') {
         Object.keys(target).forEach(key => {
             // defineReactive方法会给目标属性装上监听器
             defineReactive(target, key, target[key])
         })
     }
 }
 

 // 定义defineReactive
 function defineReactive(target, key, val) {
     const dep = new Dep();
     // 属性值也可能是object类型，
     observer(val);
     // 为当前属性安装监听器
     Object.defineProperty(target, key, {
         enumerable: true, // 可枚举
         configurable: false, //不可配置
         get: function() {
             return val;
         },
         set: function(value) {
            // console.log(`${target}属性的${key}值变成了${value}`);
            // val = value;
            // 通知所有订阅者
            dep.notify();
         }
     })
 }

 //实现订阅者Dep
 class Dep {
    constructor() {
        // 初始化订阅队列
        this.subs = [];
    }

    // 增加订阅者
    addSub(sub) {
        this.subs.push(sub);
    }
    // 通知订阅者
    notify() {
        this.subs.forEach(sub => {
            sub.update();
        })
    }

 }

 // 实现一个Event Bus/Event Emitter
 // 理解为一个事件中心，所有事件的发布订阅都不由订阅方和发布方直接沟通，必须要委托这个事件中心帮忙实现

 class EventEmitter {
     constructor() {
         // handlers是一个map，用于存储事件与回调之间的映射关系
         this.handlers = {};
     }

     // on方法用于安装监听器，接受目标事件名和回调函数作为参数
     on(eventName, cb) {
         if (!this.handlers[eventName]) {
             // 如果，没有，则手写初始化一个监听函数队列
             this.handlers[eventName] = [];
         }
         // 把回调函数推入目标事件的监听函数队列
         this.handlers[eventName].push(cb)
     }

     // emit方法用于触发目标事件，接受事件名和监听函数作为参数
     emit(eventName, ...args) {
         if (this.handlers[eventName]) {
             this.handlers[eventName].forEach(callback => {
                 callback(...args);
             })
         }
     }

     // 移除某个事件回调队列里的回调函数
     off(eventName, cb) {
         const callbacks = this.handlers[eventName];
         const index = callbacks.indexOf(cb);
         if (index !== -1 ) {
             callbacks.splice(index, 1)
         }
     }

     // 为事件注册单次监听器
     once(eventName, cb) {
         // 对回调进行包装，使其执行完毕后自动移除
         const wrapper = (...args) => {
             cb.apply(...args);
             this.off(eventName, wrapper)
         }
         this.on(eventName, wrapper);
     }
 }

 // 观察者与发布-订阅的区别：
 // 观察者_场景：产品将相关的开发人员拉到一个群，然后发需求文档到群上，这样每个人都可以拿到需求文档
 // 这种场景属于发布者直接接触订阅者，属于观察者模式

 // 发布订阅_场景：产品直接将需求文档上传到公司需求平台，然后该平台感知到文件的变化，自动通知每一个订阅了该文件的开发者
 // 发布者不直接接触订阅者，而是统一由第三方完成实际的通信操作，属于发布订阅

 // 区别： 1. 发布者是否直接接触订阅者 2. 是否存在第三方
 // 观察者减少解耦，但并没有完全解耦（被观察者必须维护一套观察者集合）
 // 发布订阅实现了完全解耦